// Package dao
/**
* @Project : geektime-basic-go-study
* @File    : article.go
* @IDE     : GoLand
* @Author  : Tvux
* @Date    : 2024/11/9 10:59
**/

package article

import (
	"context"
	"fmt"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
	"time"
)

// Article 如何设计索引，只需要思考一条 SQL 语句的 WHERE 查询条件即可
// 比如：我想查询某个创作者下的所有文章
// SELECT * FROM articles WHERE author_id = 1
// 此时我会想到在 AuthorId 上创建索引，即 AuthorId int 64 `gorm:"index"`
// 假设现在有一个需求，产品定的:) 查询出来的文章列表要按照更新时间倒序排序
// SELECT * FROM articles WHERE author_id = 1 ORDER BY `utime` DESC
// 那么此时就应该是要在 AuthorId 和 Utime 上创建联合索引
type Article struct {
	Id       int64  `gorm:"primaryKey, autoIncrement"`
	Title    string `gorm:"type:varchar(1024)"`
	Content  string `gorm:"BLOB"` // blob 用于存储二进制大对象
	AuthorId int64  `gorm:"index:auid_utime"`
	Status   uint8
	CTime    int64
	UTime    int64 `gorm:"index:auid_utime"`
}

// PublishedArticle 同库不同表，表示线上库，即读者看到的帖子表
// 现在考虑使用组合，后续如果有新增或减少字段再考虑修改
type PublishedArticle struct {
	Article
}

type ArticleDao interface {
	Insert(ctx context.Context, article Article) (int64, error)
	UpdateByIdAndAuthorId(ctx context.Context, article Article) error
	Sync(ctx context.Context, article Article) (int64, error)
	Upsert(ctx context.Context, article PublishedArticle) error
}

var (
	_ ArticleDao = (*GormArticleDAO)(nil)
)

type GormArticleDAO struct {
	db *gorm.DB
}

func (Self *GormArticleDAO) Upsert(ctx context.Context, article PublishedArticle) error {
	now := time.Now().UnixMilli()
	article.CTime = now // 如果是第一次插入，则需要该字段
	article.UTime = now // 如果是第一次插入或更新，则需要该字段
	return Self.db.WithContext(ctx).Clauses(clause.OnConflict{
		// 还有一些别的字段，这些字段都是 SQL 2003 标准要求提供的
		// INSERT aaa ON CONFLICT(bbb) DO NOTHING
		// INSERT aaa ON CONFLICT(bbb) DO UPDATES ccc WHERE ddd
		// Columns 哪些列遇到冲突
		//Columns: []clause.Column{clause.Column{Name: "id"}},
		// 遇到数据冲突且满足 where 条件的才会更新
		// 在 MySQL 中，数据冲突指的是主键或索引冲突
		// 在 sqlite 或者 PostgreSQL 中，数据冲突指的是，对应的列已经有值了，除了可以指定列，此时还可以指定索引
		//Where:
		// 遇到冲突什么也不做
		// DoNothing
		// 如果只是 MySQL，则只需要关心 DoUpdates 这个字段
		DoUpdates: clause.Assignments(map[string]interface{}{
			"title":   article.Title,
			"content": article.Content,
			"u_time":  now,
		}),
	}).Create(&article).Error
	// MySQL 最终的 SQL 语句是 INSERT xxx ON DUPLICATE KEY UPDATE xxx
	// MySQL 无法指定列或者索引，只有 sqlite 跟 PostgreSQL 以及 oracle 才可以，即 ON CONFLICT(xxx)
}

func (Self *GormArticleDAO) Sync(ctx context.Context, article Article) (int64, error) {
	// 在同一个 dao 中操作，应该操作的是同库但不同的两张表（制作表和线上表）
	// 如果有多个数据源，最好是拆分成两个 dao
	// 在事务内部，这里采用了闭包形态 GORM 帮助我们管理了事务的生命周期
	// 即 Begin RollBack Commit 都不需要我们去操心
	id := article.Id
	err := Self.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
		var err error
		txDao := NewGormArticleDAO(tx)
		// 为什么这里制作库不同样写成 upsert
		// 核心在于 需要保证只有作者才能更新自己的文章，其他用户不允许
		// 但是 MySQL 不允许 ON DUPLICATE UPDATE xxx WHERE `author_id` = yyy
		if id > 0 {
			err = txDao.UpdateByIdAndAuthorId(ctx, article)
		} else {
			id, err = txDao.Insert(ctx, article)
		}
		if err != nil {
			return err
		}

		return txDao.Upsert(ctx, PublishedArticle{Article: article})
	})
	return id, err
}

func (Self *GormArticleDAO) UpdateByIdAndAuthorId(ctx context.Context, article Article) error {
	article.UTime = time.Now().UnixMilli()
	res := Self.db.WithContext(ctx).Model(&article).
		Where("id = ? AND author_id = ?", article.Id, article.AuthorId).
		Updates(map[string]any{
			"title":   article.Title,
			"content": article.Content,
			"u_time":  article.UTime,
		})
	if res.Error != nil {
		return res.Error
	}
	if res.RowsAffected == 0 {
		return fmt.Errorf("更新失败，用户 author_id=%d 尝试修改文章 id=%d", article.AuthorId, article.Id)
	}
	return nil
}

func NewGormArticleDAO(db *gorm.DB) ArticleDao {
	return &GormArticleDAO{
		db: db,
	}
}

func (Self *GormArticleDAO) Insert(ctx context.Context, article Article) (int64, error) {
	now := time.Now().UnixMilli()
	article.CTime = now
	article.UTime = now
	err := Self.db.WithContext(ctx).Create(&article).Error
	return article.Id, err
}
